/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.lucene.spatial.geometry;

/**
 * Class representing a latitude longitude pair.
 */
public class LatLng {

	public final static int LONGITUDE_DEGREE_RANGE = 360;
	public final static int LONGITUDE_DEGREE_MIN = -LONGITUDE_DEGREE_RANGE/2;
	public final static int LONGITUDE_DEGREE_MAX = LONGITUDE_DEGREE_RANGE/2;
	public final static int LATITUDE_DEGREE_RANGE = 180;
	public final static int LATITUDE_DEGREE_MIN = -LATITUDE_DEGREE_RANGE/2;
	public final static int LATITUDE_DEGREE_MAX = LATITUDE_DEGREE_RANGE/2;

  private final double lat;
  private final double lng;

  /**
   * Creates a new LatLng with the given latitude and longitude which are assumed to not be normalized
   *
   * @param lat Latitude part of the LatLng
   * @param lng Longitude part of the LatLng
   */
  public LatLng(double lat, double lng) {
	    if (lat > LATITUDE_DEGREE_MAX || lat < LATITUDE_DEGREE_MIN) {
        throw new IllegalArgumentException("Illegal latitude value " + lat);
      }

	    this.lat = lat;

	    if (lng > LONGITUDE_DEGREE_MAX || lng < LONGITUDE_DEGREE_MIN) {
	    	 lng = lng % LONGITUDE_DEGREE_RANGE;
	    	 if (lng > LONGITUDE_DEGREE_MAX) {
	    		 this.lng = lng - LONGITUDE_DEGREE_RANGE;
	    	 } else if (lng < LONGITUDE_DEGREE_MIN) {
	    		 this.lng = lng + LONGITUDE_DEGREE_RANGE;
	    	 } else {
	    		 this.lng = lng;
	    	 }
	    } else {
	    	this.lng = lng;
	    }
  }

  /**
   * Calculates the distance between two lat/lng's in miles.
   *
   * @param latLng Second lat,lng position to calculate distance to.
   * @return Returns the distance in miles.
   */
  public double arcDistance(LatLng latLng) {
    return arcDistance(latLng, DistanceUnits.MILES);
  }

  /**
   * Calculates the distance between two lat/lng's in miles or kilometres
   *
   * @param targetLatLng Second lat,lng position to calculate distance to.
   * @param distanceUnit Units to calculate distace, defaults to miles
   * @return Returns the distance in meters or miles.
   */
  public double arcDistance(LatLng targetLatLng, DistanceUnits distanceUnit) {

    double srcLat = Math.toRadians(getLat());
    double srcLng = Math.toRadians(getLng());
    double targetLat = Math.toRadians(targetLatLng.getLat());
    double targetLng = Math.toRadians(targetLatLng.getLng());

    return Math.acos(Math.sin(srcLat) * Math.sin(targetLat) + Math.cos(srcLat) * Math.cos(targetLat) * Math.cos(targetLng - srcLng)) * distanceUnit.earthRadius();
  }
  
  public static LatLng computeDestination(LatLng startPoint, double distance, double heading) {
	  return computeDestination(startPoint,distance,heading,DistanceUnits.MILES);
  }
  
  public static LatLng computeDestination(LatLng startPoint, double distance, double heading, DistanceUnits distanceUnit) {
	    double startPointLatitude = startPoint.getLat();
	    double startPointLongitude = startPoint.getLng();
	    double earthRadius = distanceUnit.earthRadius();
	    double headingRad = Math.toRadians(heading);
	    double startPointLatitudeRad = Math.toRadians(startPointLatitude);
	    double startPointLongitudeRad = Math.toRadians(startPointLongitude);

	    // Haversine formula (http://www.movable-type.co.uk/scripts/latlong.html)
	    double destinationLatitudeRad = Math.asin(Math.sin(startPointLatitudeRad) * Math.cos(distance / earthRadius) +
	                             Math.cos(startPointLatitudeRad) * Math.sin(distance / earthRadius) * Math.cos(headingRad));

	    double destinationLongitudeRad = startPointLongitudeRad + Math.atan2(Math.sin(headingRad) * Math.sin(distance / earthRadius) * Math.cos(startPointLatitudeRad),
	                                    Math.cos(distance / earthRadius) - Math.sin(startPointLatitudeRad) * Math.sin(destinationLatitudeRad));

	    return new LatLng(Math.toDegrees(destinationLatitudeRad), Math.toDegrees(destinationLongitudeRad));
	}

  /**
   * {@inheritDoc}
   */
  @Override
  public int hashCode() {
    return new Double(lng).hashCode() ^ new Double(lat).hashCode();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean equals(Object obj) {
    if (!LatLng.class.isInstance(obj)) {
      return false;
    }

    LatLng latLng = (LatLng) obj;
    //Equals with double is always subject to suspiscion (precision and/or multiple representation of the same number : 1.0*10^3 and 10*10^2)
    return latLng.getLat() == lat && latLng.getLng() == lng;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    return "[" + getLat() + "," + getLng() + "]";
  }

  // ================================================= Getters / Setters =============================================

  /**
   * Returns the latitude part of the LatLng
   *
   * @return Latitude part of the LatLng
   */
  public double getLat() {
    return this.lat;
  }

  /**
   * Returns the longitude part of the LatLng
   *
   * @return Longitude part of the LatLng
   */
  public double getLng() {
    return this.lng;
  }
}
